{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## <center>Мини-проект. Полный анализ данных и построение прогнозной модели модели \n",
    "<center> Автор: Григорий Демин"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В рамках данного задания используется набор данных по директ-маркетингу португальского банка. Ссылка: [Bank Marketing Data Set](http://archive.ics.uci.edu/ml/datasets/Bank+Marketing). "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from __future__ import division, print_function\n",
    "# отключим всякие предупреждения Anaconda\n",
    "import warnings\n",
    "warnings.filterwarnings('ignore')\n",
    "%pylab inline\n",
    "import seaborn as sns\n",
    "import pandas as pd"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "__Описание набора данных__\n",
    "\n",
    "Данные описывают директ-маркетинговую кампанию португальского банка. Кампания заключалась в обзвоне клиентов и предложения им депозита. Достаточно часто одному и тому же клиенту делалось несколько звонков.\n",
    "Целью задачи является предсказание, воспользуется ли клиент депозитом или нет.\n",
    "\n",
    "\n",
    "__Список переменных:__\n",
    "* 1 - Возраст (шкала)\n",
    "* 2 - job : Род занятий (номинальная: \"admin.\" (админ. персонал),\"unknown\" (неизвестно),\"unemployed\" (безработный),\"management\" (менеджмент),\"housemaid\" (домохозяйка),\"entrepreneur\" (предприниматель),\"student\" (учащийся),\"blue-collar\" (служащий),\"self-employed\" (самозанятый),\"retired\" (пенсионер),\"technician\",\"services\" (сервис) \n",
    "* 3 - marital : Семейное положение (номинальная: \"married\" (женат/замужем),\"divorced\" (разведен(а)/вдова/вдовец),\"single\" (одинокий))\n",
    "* 4 - education: Образование (номинальная: \"unknown\" (неизвестно),\"secondary\" (среднее),\"primary\" (начальное),\"tertiary\" (высшее))\n",
    "* 5 - default: была ли просрочка по кредиту? (бинарная: \"yes\" (да),\"no\" (нет))\n",
    "* 6 - balance: среднегодовой баланс на счету, в евро (число) \n",
    "* 7 - housing: есть ли ипотека? (бинарная: \"yes\" (да),\"no\" (нет))\n",
    "* 8 - loan: есть ли личные кредиты? (бинарная: \"yes\" (да),\"no\" (нет))\n",
    "* _далее переменные, связанные с предыдущими контактами с данным клиентом:_\n",
    "* 9 - contact: тип коммуникации (номинальная: \"unknown\" (неизвестно),\"telephone\" (стационарный телефон),\"cellular\"(сотовый)) \n",
    "* 10 - day: число месяца, когда был последний контакт (число)\n",
    "* 11 - month: месяц, когда был последний контакт (Номинальная: \"jan\", \"feb\", \"mar\", ..., \"nov\", \"dec\")\n",
    "* 12 - duration: длительность последнего контакта, в секундах (число)\n",
    "* _другие атрибуты:_\n",
    "* 13 - campaign: количество контактов, которое было с данным клиентом в данной кампании (число, включает последний контакт)\n",
    "* 14 - pdays: количество дней, которое прошло с последнего контакта данной кампании (число, -1 обозначает, что ранее контактов не было)\n",
    "* 15 - previous: количество контактов, которое было с данным клиентом до этой кампании (число)\n",
    "* 16 - poutcome: результат предыдущей кампании (номинальная: \"unknown\" (неизвестно),\"other\" (другое),\"failure\" (неудача),\"success\" (успех))\n",
    "\n",
    "  Целевая переменная:\n",
    "  17 - y: Открыл ли клиент депозит? (бинарная: 1, 0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data = pd.read_csv(\"../../data/bank.csv\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data.head() "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Выводим основные харакетристики переменных;**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(data.shape)\n",
    "data.describe(include = \"all\").T"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# частотки по категориальным переменным\n",
    "categorical = [\"marital\",\"education\",\"default\",\"housing\",\"loan\",\"contact\",\"month\",\"poutcome\"]\n",
    "for each_var in categorical:\n",
    "    print('********')\n",
    "    print('*',each_var,'*')\n",
    "    print(data[each_var].value_counts())\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Делаем визуализацию:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.pairplot(data)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Далее предобработка данных. \n",
    "* Удалим переменную duration - это длительность последнего контакта, если она равна нулю, то явно, что успеха не было. Из-за этого у неё хорошая прогнозная сила, но её значение не известно до звонка.\n",
    "* pdays - количество дней, прошедшее с последнего звонка. Если -1, то звонка не было. Соответсвенно, перекодируем -1 в отдельную переменную - \"Был звонок/не был\", а в исходной переменной -1 заменим на медиану.\n",
    "* В дополнение к непрерывному возрасту сделаем еще возраст категориальный.\n",
    "* Перекодируем все категориальные переменные в dummies (0,1).\n",
    "* Масштабируем все переменные.\n",
    "* Разобъем выборку на тестовую и обучающую."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "### Обрабатываем pdays:\n",
    "first_call = (data.pdays == -1).astype(int)\n",
    "first_call.name = \"first_call\"\n",
    "data.pdays[data.pdays==-1] = NaN\n",
    "data.pdays = data.pdays.fillna(value = data.pdays.median())\n",
    "### Делаем возрастные категории\n",
    "age1 = (data.age<25).astype(int)\n",
    "age2 = (data.age>50).astype(int)\n",
    "age1.name = \"age1\"\n",
    "age2.name = \"age2\"\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "### dummy переменные:\n",
    "data_dummies = pd.concat([\n",
    "                        pd.get_dummies(data.job     , prefix = 'job'),\n",
    "                        pd.get_dummies(data.marital , prefix = 'marital'),\n",
    "                        pd.get_dummies(data.education, prefix = 'education'),\n",
    "                        pd.get_dummies(data.default , prefix = 'default'),\n",
    "                        pd.get_dummies(data.housing , prefix = 'housing'),\n",
    "                        pd.get_dummies(data.loan , prefix = 'loan'),\n",
    "                        pd.get_dummies(data.contact , prefix = 'contact'),\n",
    "                        pd.get_dummies(data.month   , prefix = 'month'),\n",
    "                        pd.get_dummies(data.poutcome, prefix = 'poutcome'),\n",
    "                        data.age            ,\n",
    "                        data.balance        ,\n",
    "                        data.day            ,\n",
    "                        data.campaign       ,\n",
    "                        data.pdays          ,\n",
    "                        data.previous       ,\n",
    "                        first_call,\n",
    "                        age1,\n",
    "                        age2], axis=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Разбиваем на тестовую и обучающую выборку"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.cross_validation import train_test_split\n",
    "X_train, X_test, y_train, y_test = train_test_split(data_dummies, data.y, \n",
    "                                                    test_size=0.3, \n",
    "                                                    random_state=20160212,\n",
    "                                                   stratify = data.y)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Масштабируем переменные"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn import preprocessing\n",
    "X_train = preprocessing.scale(X_train)\n",
    "X_test = preprocessing.scale(X_test)\n",
    "### Конвертируем назад в Pandas DataFrame\n",
    "X_train = pd.DataFrame(X_train)\n",
    "X_train.columns = data_dummies.columns\n",
    "X_test = pd.DataFrame(X_test)\n",
    "X_test.columns = data_dummies.columns"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Посмотрим на baseline - классификацию случайным лесом без настройки параметров.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.ensemble import RandomForestClassifier\n",
    "from sklearn.metrics import accuracy_score, confusion_matrix, f1_score\n",
    "forest = RandomForestClassifier(n_estimators=500)\n",
    "forest.fit(X_train, y_train)\n",
    "test_pred = forest.predict(X_test)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Убедимся в том, что для данной задачи доля правильных ответов не будет хорошей метрикой качества модели."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "accuracy_score(y_test, test_pred)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Однако, лучшее качество дает даже тривиальный прогноз, что клиент не откроет депозит."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_test.value_counts()[0] / y_test.shape[0]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "f1_score(y_test, test_pred)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "confusion_matrix(y_test, test_pred)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Выберем наиболее важные переменные**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "forest = RandomForestClassifier(n_estimators=1000, max_depth = 5,\n",
    "                                random_state=42).fit(X_train, y_train)\n",
    "\n",
    "features = pd.DataFrame(forest.feature_importances_,\n",
    "                        index=X_train.columns, \n",
    "                        columns=['Importance']).sort(['Importance'], \n",
    "                                                     ascending=False)\n",
    "features"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.plot(range(len(features.Importance.tolist())), \n",
    "         features.Importance.tolist())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Выбираем 20 признаков. Конвертируем выборки в Numpy матрицы."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "selected_attr = features.index.tolist()[0:20]\n",
    "X_train = X_train[selected_attr].as_matrix()\n",
    "X_test = X_test[selected_attr].as_matrix()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Попробуем четыре разных классификатора: логистическую регрессию, Gradient boosting, Random Forest и SVM. Так как у нас сильный дисбаланс в выборке (успехов только 10%), то в качестве меры будем использовать F1 score."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.ensemble import GradientBoostingClassifier\n",
    "from sklearn.ensemble import RandomForestClassifier\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.svm import SVC\n",
    "from sklearn.metrics import f1_score\n",
    "\n",
    "classifiers = [LogisticRegression(),\n",
    "               GradientBoostingClassifier(), \n",
    "               RandomForestClassifier(), \n",
    "               SVC()]\n",
    "classifiers_name = [\"LogisticRegression\",\n",
    "               \"GradientBoostingClassifier\", \n",
    "               \"RandomForestClassifier\", \n",
    "               \"SVC\"]\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Настраиваем параметры выбранных алгоритмов с помощью GridSearchCV и выбираем лучший классификатор."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.grid_search import GridSearchCV\n",
    "from sklearn.cross_validation import StratifiedKFold\n",
    "\n",
    "n_folds = 5\n",
    "scores = []\n",
    "fits = []\n",
    "logistic_params = {'penalty': ('l1', 'l2'), \n",
    "               'C': (.01,.1,1,5)}\n",
    "gbm_params = { 'n_estimators': [100, 300, 500],\n",
    "               'learning_rate':(0.1, 0.5, 1),\n",
    "                'max_depth': list(range(3, 6)), \n",
    "               'min_samples_leaf': list(range(10, 31, 10))}\n",
    "forest_params = {'n_estimators': [100, 300, 500],\n",
    "                'criterion': ('gini', 'entropy'), \n",
    "               'max_depth': list(range(3, 6)), \n",
    "               'min_samples_leaf': list(range(10, 31, 10))}\n",
    "\n",
    "svm_param = {'kernel' : ('linear', 'rbf'),\n",
    "              'C': (.5, 1, 2)           \n",
    "             }\n",
    "params = [logistic_params, gbm_params, forest_params, svm_param]\n",
    "\n",
    "# Проводим кросс-валидацию для всех моделей\n",
    "for i, each_classifier in enumerate(classifiers):\n",
    "    clf = each_classifier\n",
    "    clf_params = params[i]\n",
    "    grid = GridSearchCV(clf, clf_params, \n",
    "                        cv=StratifiedKFold(y_train, n_folds=n_folds,\n",
    "                        shuffle=False, random_state=42), \n",
    "                        n_jobs=-1, scoring=\"f1\")\n",
    "    grid.fit(X_train, y_train)\n",
    "    fits.append(grid.best_params_)\n",
    "    clf_best_score = grid.best_score_\n",
    "    scores.append(clf_best_score)\n",
    "    print(classifiers_name[i], clf_best_score, \"\\n\", grid.best_params_)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Печатаем параметры лучшего\n",
    "grid_value = max(scores)\n",
    "grid_index = [i for i in xrange(len(scores)) if scores[i]==grid_value][0]\n",
    "print(\"Лучший классификатор при GridSearch:\",\n",
    "      classifiers_name[grid_index], grid_value)\n",
    "print(fits[grid_index])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Для лучшего классификатора и набора параметров для него выберем более мелкую сетку (для того чтобы уточнить лучшие значения параметров).**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "clf_params = {'n_estimators': (250, 300, 350), \n",
    "              'learning_rate': (0.75, 1, 1.25, 1.5), \n",
    "              'min_samples_leaf': list(range(1, 14, 3))}\n",
    "\n",
    "clf = classifiers[grid_index]\n",
    "grid = GridSearchCV(clf, clf_params, cv=n_folds, \n",
    "                    n_jobs=-1, scoring=\"f1\")\n",
    "grid.fit(X_train, y_train)\n",
    "clf_best_score = grid.best_score_\n",
    "clf_best_params = grid.best_params_\n",
    "clf_best = grid.best_estimator_\n",
    "mean_validation_scores = []\n",
    "print(\"Лучший результат\", clf_best_score, \n",
    "      \"лучшие параметры\", clf_best_params)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Строим график кривой обучения**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_with_std(x, data, **kwargs):\n",
    "        mu, std = data.mean(1), data.std(1)\n",
    "        lines = plt.plot(x, mu, '-', **kwargs)\n",
    "        plt.fill_between(x, mu - std, mu + std, edgecolor='none',\n",
    "                         facecolor=lines[0].get_color(), alpha=0.2)\n",
    "        \n",
    "def plot_learning_curve(clf, X, y, scoring, cv=5):\n",
    " \n",
    "    train_sizes = np.linspace(0.05, 1, 20)\n",
    "    n_train, val_train, val_test = learning_curve(clf,\n",
    "                                                  X, y, train_sizes, cv=cv,\n",
    "                                                  scoring=scoring)\n",
    "    plot_with_std(n_train, val_train, label='training scores', c='green')\n",
    "    plot_with_std(n_train, val_test, label='validation scores', c='red')\n",
    "    plt.xlabel('Training Set Size'); plt.ylabel(scoring)\n",
    "    plt.legend()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_learning_curve(GradientBoostingClassifier(n_estimators=2, \n",
    "                    learning_rate=1.5, min_samples_leaf=7),\n",
    "                   X_train, y_train, scoring='f1', cv=10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Построим валидационную кривую для данных параметров бустинга. В качестве параметра сложности будем использовать learning_rate:**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.learning_curve import validation_curve\n",
    "\n",
    "def plot_validation_curve(clf, X, y, cv_param_name, \n",
    "                          cv_param_values, scoring):\n",
    "\n",
    "    val_train, val_test = validation_curve(clf, X, y, cv_param_name,\n",
    "                                           cv_param_values, cv=5,\n",
    "                                                  scoring=scoring)\n",
    "    plot_with_std(cv_param_values, val_train, \n",
    "                  label='training scores', c='green')\n",
    "    plot_with_std(cv_param_values, val_test, \n",
    "                  label='validation scores', c='red')\n",
    "    plt.xlabel(cv_param_name); plt.ylabel(scoring)\n",
    "    plt.legend()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "learning_rates = np.linspace(0.1, 2.3, 20)\n",
    "plot_validation_curve(GradientBoostingClassifier(n_estimators=250, \n",
    "                    min_samples_leaf=7), X_train, y_train, \n",
    "                      cv_param_name='learning_rate', \n",
    "                      cv_param_values=learning_rates,\n",
    "                   scoring='f1')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "final_gbm = GradientBoostingClassifier(n_estimators=250, \n",
    "                    min_samples_leaf=7, learning_rate=1.5)\n",
    "final_gbm.fit(X_train, y_train)\n",
    "final_pred = final_gbm.predict(X_test)\n",
    "accuracy_score(y_test, final_pred), f1_score(y_test, final_pred)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "__Выводы.__\n",
    "\n",
    "Построена модель предсказания, согласится ли респондент на банковский продукт. Модель предсказывает с 87%-ной долей правильных ответов на отложенных 30% выборки. Но accuracy не очень хорошо характеризует качество модели из-за сильного дисбаланса в целевой переменной (~90% против ~10%), поэтому в качестве целевой была выбрана метрика F1-score. На отложенной выборке удалось добиться только F1=0.22.\n",
    "Построены кривые обучения и валидационные кривые. К сожалению, у них наблюдается некоторая немонотонность, однако видно, что увеличение количества примеров более 2000 не приносит существенной выгоды (у нас в обучающей выборке более 3000 примеров). Из валидационного графика видно, что learning rate = 1.5, выбранный с помощью перебора на обучающей выборки, близок к оптимальному.  "
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.1"
  },
  "name": "lesson4_homework_uci.ipynb"
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
